package roborally.itemsonboard;


import roborally.board.Board;
import roborally.factory.PhysicalAmountFactory;
import roborally.supporting.DefaultConstants;
import roborally.supporting.PhysicalAmount;
import roborally.supporting.Position;
import roborally.supporting.Unit;
import roborally.usableitems.Usable;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A battery contains everything from an EnergyItem and thus also from an boardItem. Also, it has a weight. A battery
 * can be used by an Robot. The energy of the battery will be added to the energy of 
 * the Robot if it is possible.
 * 
 * @invar The weight of the battery must be valid.
 * 		| Battery.isValidWeight(this.getWeigh())
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 4.0
 */
public class Battery extends EnergyItem implements Usable {
	
//Constructors ---------------------------------------------------------------------------------------	
	
	/**
	 * Creates a new battery at the given board and location, with the given weight, energy limit and energy level.
	 * 
	 * @param weight The weight of the battery.
	 * @param energyLimit The energyLimit of this battery.
	 * @param energyLevel The energyLevel of this battery.
	 * @param board The board at which this battery should be, null is allowed.
	 * @param position The position at which this battery should be.
	 * @pre The energyLimit must be possible.
	 * 		| isPossibleEnergyLimit(energyLimit)
	 * @pre The energyLevel must be positive and smaller than or equal to energyLimit
	 * 		| canHaveAsEnergyLevel(energyLevel)
	 * @effect 
	 * 		| super(energyLimit, energyLevel, board, position)
	 * @post 
	 * 		| if !isValidWeigh(weight)
	 * 		| 	then (new this).getWeight() == DefaultConstants.BATTERY_WEIGHT
	 * 		| else
	 * 		| 	(new this).getWeigh() == weight
	 * @throws IllegalArgumentException
	 * 		Position is null or the board can't have this new boardItem.
	 * 		| position== null || ( board != null && !board.canHaveAsItem(new this) )
	 * 
	 */
	public Battery(PhysicalAmount weight, PhysicalAmount energyLimit, PhysicalAmount energyLevel, Board board, Position position){
		super(energyLimit, energyLevel, board, position);
		if(!isValidWeight(weight))
			weight = DefaultConstants.BATTERY_WEIGHT;
		this.weight = weight;
	}
	
	/**
	 * Creates a new battery with the given options.
	 * 
	 * @param weight The weight of the battery.
	 * @param energyLevel The energyLevel of the battery.
	 * @param board The board of the battery.
	 * @param position The position of the battery.
	 * @effect 
	 * 		| this(weight, DefaultConstants.BATTERY_ENERGY_LIMIT , energyLevel, board, position)
	 */
	public Battery(PhysicalAmount weight, PhysicalAmount energyLevel, Board board, Position position) 
			throws IllegalArgumentException {
		this(weight, DefaultConstants.BATTERY_ENERGY_LIMIT , energyLevel, board, position);
	}
	
	/**
	 * Creates a new battery with the given energy limit and energy level, with no board an located at the origin.
	 * 
	 * @param weight The weight of this battery.
	 * @param energyLimit The energyLimit of this battery.
	 * @param energyLevel The energyLevel of this battery.
	 * @effect 
	 * 		| this(weight, DefaultConstants.BATTERY_ENERGY_LIMIT, energyLevel, null, Position.Origin)
	 */
	public Battery(PhysicalAmount weight, PhysicalAmount energyLevel) 
			throws IllegalArgumentException {
		this(weight, DefaultConstants.BATTERY_ENERGY_LIMIT, energyLevel, null, Position.ORIGIN);
	}
	
	/**
	 * Creates a new battery with given weight and zero energyLevel, no board and located at the origin.
	 * 
	 * @param weight The weight of the battery.
	 * @effect
	 * 		| this(weight, PhysicalAmount.ENERGY_ZERO)
	 */
	public Battery(PhysicalAmount weight) throws IllegalArgumentException {
		this(weight, PhysicalAmount.ENERGY_ZERO);
	}
	
	/**
	 * Creates a default battery.
	 * 
	 * @effect
	 * 		| this(DefaultConstants.BATTERY_WEIGHT)
	 */
	public Battery() {
		this(DefaultConstants.BATTERY_WEIGHT);
	}
	
//Energy ----------------------------------------------------------------------------------------------------	
	
	/**
	 * Controls if this can have the given energyLimit.
	 * 
	 * @param energyLimit The energyLimit to be controlled.
	 * @return False if the energyLimit is null.
	 * 		| if energyLimit == null
	 * 		|	then result == false
	 * @return False if the units are different from Ws.
	 * 		| if energyLimit.getUnit() != Unit.WATTSECOND
	 * 		|	then result == false
	 * @return False if the energyLimit has zero as value.
	 * 		| if energyLimit.equals(EnergyAmount.ZERO)
	 * 		|	then result == false
	 * @return False if the energyLimit is negative.
	 * 		| if energyLimit.getAmount() < 0
	 * 		|	then result == false
	 * @return False if the energyLimit is greater then the default one.
	 * 		| if energyLimit.compareTo(DefaultConstants.BATTERY_ENERGY_LIMIT) != 1
	 * 		|	then result == false
	 */
	@Override
	public boolean isPossibleEnergyLimit(PhysicalAmount energyLimit) {
		return super.isPossibleEnergyLimit(energyLimit) && 
				energyLimit.compareTo(DefaultConstants.BATTERY_ENERGY_LIMIT) != 1;
	}
	
//Position --------------------------------------------------------------------------------------------------	
	
	/**
	 * Checks whether this battery can share a position with the given boardItem.
	 * 
	 * @param boardItem The item which is at the same position as this battery.
	 * @return False if boardItem is null.
	 * 		| if boardItem == null
	 * 		|	then result == false
	 * @return False if boardItem is a Wall.
	 * 		| if Wall.class.isInstance(boardItem)
	 *		|	then result == false
	 * @return True if boardItem is a battery.
	 * 		| if Battery.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a robot.
	 * 		| if Robot.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return When another type of boardItem is created, in the future, he is responsible for handling this.
	 * 		| result == boardItem.canHaveItemOnSamePosition(this)
	 */
	@Override
	public boolean canHaveItemOnSamePosition(BoardItem boardItem) {
		if(boardItem == null)
			return false;
		else if(Wall.class.isInstance(boardItem))
			return false;
		else if(Battery.class.isInstance(boardItem))
			return true;
		else if(Robot.class.isInstance(boardItem))
			return true;
		else
			return boardItem.canHaveItemOnSamePosition(this);
	}
	
//Weight ------------------------------------------------------------------------------------------------------	
	
	/**
	 * Returns the weight of this battery.
	 */
	@Basic @Raw @Immutable @Override
	public PhysicalAmount getWeight(){
		return this.weight;
	}
	
	/**
	 * Checks the weight.
	 * 
	 * @param weight The weight to be checked.
	 * @return False if the weight is null
	 * 		| if weight == null
	 * 		|	then result == false
	 * @return False if the unit of the weight can not converted to Unit.GRAM
	 * 		| if !weight.getUnit().canConvertTo(Unit.GRAM)
	 * 		|	then result == false
	 * @return False if the weight amount is negative.
	 * 		| if weight.getAmount() < 0
	 * 		|	then result == false.
	 */
	@Raw
	public static boolean isValidWeight(PhysicalAmount weight){
		if(weight == null)
			return false;
		else if(!weight.getUnit().canConvertTo(Unit.GRAM))
			return false;
		else if(weight.getAmount() < 0)
			return false;
		return true;
	}
	
	/**
	 * An PhysicalAmount representing the weight of this Battery.
	 */
	private final PhysicalAmount weight;

//Usable -----------------------------------------------------------------------------------------------------	
	
	/**
	 * Transfers energy from this battery to the robot if the robot is carrying this battery.
	 * 
	 * @param robot The robot to which the energy of this battery must go.
	 * @pre
	 * 		| robot != null
	 * @pre 
	 * 		| robot.canUse(this)
	 * @pre
	 * 		| !isTerminated()
	 * @effect 
	 * 		| if robot.canHaveAsEnergyLevel(robot.getEnergyLevel().addEnergyAmounts(this.getEnergyLevel()))
	 * 		| 	then robot.recharge(this.getEnergyLevel())
	 * 		| else
	 * 		|	robot.setEnergyLevel(robot.getEnergyLimit())
	 * @effect 
	 * 		| if robot.canHaveAsEnergyLevel(robot.getEnergyLevel().addEnergyAmounts(this.getEnergyLevel()))
	 * 		| 	then this.setEnergyLevel(0)
	 * 		| else
	 * 		|	this.setEnergyLevel( this.getEnergyLevel().subtractEnergyAmounts( (robot.getEnergyLimit().subtractEnergyAmounts(robot.getEnergyLevel())) ) )
	 */
	public void isUsed(Robot robot) {
		assert robot != null;
		assert robot.canUse(this);
		assert !isTerminated();
		
		if(robot.canHaveAsEnergyLevel(robot.getEnergyLevel().add(this.getEnergyLevel()))){
			robot.recharge(this.getEnergyLevel());
			this.setEnergyLevel(PhysicalAmountFactory.get().create(0, this.getEnergyLevel().getUnit())); //preconditions are dealt with, so they disappear in this method.
		}
		else{
			this.setEnergyLevel( this.getEnergyLevel().subtract( (robot.getEnergyLimit().subtract(robot.getEnergyLevel())) ) ); // can never become negative.
			robot.setEnergyLevel(robot.getEnergyLimit());
		}	
	}

	/**
	 * Let the robot pick up this battery, if possible.
	 * 
	 * @param robot The robot that should pick up this battery.
	 * @pre
	 * 		| robot != null
	 * @pre 
	 * 		| robot.canPickUp(this)
	 * @pre
	 * 		| !isTerminated()
	 * @post
	 * 		| !robot.getBoard().contains(this)
	 */
	 @Override
	public void isPickedUp(Robot robot) {
		assert robot != null;
		assert robot.canPickUp(this);
		assert !isTerminated();
		robot.getBoard().removeAsItem(this);
	}
	
	/**
	 * Let the robot drop this battery, if possible.
	 *  
	 * @param robot The robot that should drop this battery.
	 * @pre
	 * 		| robot != null
	 * @pre 
	 * 		| robot.canDrop(this)
	 * @effect 
	 * 		| if robot.getBoard() != null
	 * 		|	then robot.getBoard().addAsItem(this)
	 * @post
	 * 		| if robot.getBoard() == null
	 * 		|	this.getBoard() == null
	 * @post 
	 * 		| this.getPosition() == robot.getPosition()
	 * @note The robot will leave this method in a raw state because this battery will have a board and
	 * a position, and the robot will possess this battery.
	 */
	@Raw @Override
	public void isDropped(Robot robot) {
		assert robot != null;
		assert robot.canDrop(this);
		assert !isTerminated();
		this.setPosition(robot.getPosition());
		if(robot.getBoard()!=null)
			robot.getBoard().addAsItem(this);
		else
			this.setBoard(null);
	}
	
//Object ---------------------------------------------------------------------------------------------------	
	
	/**
	 * Compares the usable to this one.
	 * 
	 * @return
	 * 		| result == this.getWeight().compareTo(usable.getWeight())
	 * @throws IllegalArgumentException
	 * 		| usable == null
	 */
	@Override
	public int compareTo(Usable usable) throws IllegalArgumentException{
		if(usable == null)
			throw new IllegalArgumentException("Cannot compare to null");
		return this.getWeight().compareTo(usable.getWeight());
	}
	
	/**
	 * Returns a textual specification of this battery.
	 * 
	 * @return A textual specification of this battery.
	 * 		| result == super.toString() + ", [Weight: " + this.getWeight() + "]"
	 */
	@Override
	public String toString() {
		return super.toString() + ", [Weight: " + this.getWeight() + "]";
	}
	
//LegalStatus ------------------------------------------------------------------------------------------------
	
	/**
	 * Controls if the battery satisfies all his class invariants.
	 * 
	 * @return False if the weight is not valid.
	 * 		| if !Battery.isValidWeight(getWeight())
	 * 		|	then result == false
	 */	
	@Override
	public boolean legalStatus() {
		return super.legalStatus() && Battery.isValidWeight(getWeight());
	}

//Shoot -----------------------------------------------------------------------------------------------------	

	/**
	 * If this is been shot, the energyLevel increases by some amount. If the energyLevel is greater
	 * than the limit, the limit one will be set.
	 * 
	 * @post If this can have the energyLevel, added by some amount, than this will have that energyLevel. Otherwise
	 * 		this will have the default energyLevel.
	 * 		| if this.canHaveAsEnergyLevel(this.getEnergyLevel().add(DefaultConstants.BATTERY_BEEN_HIT)
	 * 		|	then (new this).getEnergyLevel().equals(this.getEnergyLevel().add(DefaultConstants.BATTERY_BEEN_HIT))
	 * 		| else
	 * 		|	then (new this).getEnergyLevel().equals(this.getEnergyLimit())
	 * 		
	 */
	@Override
	public void beenShot() {
		PhysicalAmount energyLevel = this.getEnergyLevel().add(DefaultConstants.BATTERY_BEEN_HIT);
		if(this.canHaveAsEnergyLevel(energyLevel))
			this.setEnergyLevel(energyLevel);
		else
			this.setEnergyLevel(this.getEnergyLimit());
	}

}
